package bma.common.langutil.runtime;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

import bma.common.langutil.concurrent.ScheduledExecutor;
import bma.common.langutil.core.StringUtil;
import bma.common.langutil.core.ValueUtil;
import bma.common.langutil.io.FileMonitor;
import bma.common.langutil.io.FileMonitor.Event;
import bma.common.langutil.io.FileMonitorListener;
import bma.common.langutil.io.IOUtil;

public class RuntimeConfig {

	final org.slf4j.Logger log = org.slf4j.LoggerFactory
			.getLogger(RuntimeConfig.class);

	private ScheduledExecutor executor;
	private List<String> files = new LinkedList<String>();
	private long fileCheckTime = 5 * 1000;

	// runtime
	protected FileMonitor monitor;
	protected List<RuntimeConfigListener> listeners = new CopyOnWriteArrayList<RuntimeConfigListener>();

	protected static class PROP {
		public String name;
		public Properties properties;
	}

	protected List<PROP> datas = new CopyOnWriteArrayList<RuntimeConfig.PROP>();

	public ScheduledExecutor getExecutor() {
		return executor;
	}

	public void setExecutor(ScheduledExecutor executor) {
		this.executor = executor;
	}

	public List<String> getFiles() {
		return files;
	}

	public void setFiles(List<String> files) {
		this.files = files;
	}

	public void setFile(String f) {
		if (this.files == null)
			this.files = new ArrayList<String>(1);
		this.files.add(f);
	}

	public long getFileCheckTime() {
		return fileCheckTime;
	}

	public void setFileCheckTime(long fileCheckTime) {
		this.fileCheckTime = fileCheckTime;
	}

	public void setCheckTime(String s) {
		this.fileCheckTime = ValueUtil.periodValue(s, this.fileCheckTime);
	}

	public void addListener(RuntimeConfigListener lis) {
		if (!this.listeners.contains(lis)) {
			this.listeners.add(lis);
		}
	}

	public void init() {
		if (this.monitor == null) {
			if (this.files != null) {
				for (String file : files) {
					loadConfigFile(file, false);
				}

				this.monitor = new FileMonitor();
				this.monitor.setExecutor(executor);
				this.monitor.setTime(fileCheckTime);
				this.monitor.setFiles(this.files);
				this.monitor.addListener(new FileMonitorListener() {

					@Override
					public void handleEvent(String fileName, Event event) {
						loadConfigFile(fileName, true);
					}
				});
				this.monitor.start();
			}
		}
	}

	public void loadConfigFile(String fileName, boolean notify) {
		if (log.isInfoEnabled()) {
			log.info("loadConfigFile({},{})", fileName, notify);
		}
		File f = new File(fileName);
		if (!f.exists()) {
			removeConfigGroup(fileName, notify);
			return;
		}

		Properties prop = new Properties();
		InputStream in = null;
		try {
			in = new FileInputStream(f);
			prop.load(in);
		} catch (Exception e) {
			if (log.isWarnEnabled()) {
				log.warn("load Runtime config(" + fileName + ") fail", e);
			}
			return;
		} finally {
			IOUtil.close(in);
		}

		setConfigGroup(fileName, prop, notify);
	}

	public void close() {
		if (this.monitor != null) {
			this.monitor.stop();
			this.monitor = null;
		}
	}

	public void removeListener(RuntimeConfigListener lis) {
		this.listeners.remove(lis);
	}

	protected void notifyListeners(Collection<String> keys) {
		if (log.isDebugEnabled()) {
			log.debug("notifyListeners({})", keys);
		}
		if (keys == null || keys.isEmpty())
			return;
		for (RuntimeConfigListener lis : this.listeners) {
			try {
				lis.runtimeConfigChange(keys);
			} catch (Exception e) {
				if (log.isDebugEnabled()) {
					log.debug("listener(" + lis + ") handle notify", e);
				}
			}
		}
	}

	protected PROP getConfigGroup(String name) {
		for (PROP prop : this.datas) {
			if (StringUtil.equals(prop.name, name))
				return prop;
		}
		return null;
	}

	public void setConfigGroup(String name, Properties prop, boolean notify) {
		if (prop == null)
			return;
		if (log.isDebugEnabled()) {
			log.debug("setConfigGroup({}, #{},{})",
					new Object[] { name, prop.size(), notify });
		}
		Properties nprop = new Properties();
		Map<String, String> tmp = null;
		if (notify) {
			tmp = new HashMap<String, String>();
		}
		for (String n : prop.stringPropertyNames()) {
			String ov = getConfig(n);
			String nv = prop.getProperty(n);
			nprop.setProperty(n, nv);
			if (notify && ov != null) {
				tmp.put(n, ov);
			}
		}

		Properties old = null;
		PROP p = getConfigGroup(name);
		if (p == null) {
			p = new PROP();
			p.name = name;
			this.datas.add(p);
		} else {
			old = p.properties;
			if (notify) {
				if (tmp == null)
					tmp = new HashMap<String, String>();
				for (String n : old.stringPropertyNames()) {
					if (!tmp.containsKey(n)) {
						String ov = getConfig(n);
						tmp.put(n, ov);
					}
				}
			}
		}
		p.properties = nprop;

		if (notify && !prop.isEmpty()) {
			Set<String> keys = new HashSet<String>();
			for (String n : prop.stringPropertyNames()) {
				String ov = tmp.get(n);
				String nv = getConfig(n);
				if (!StringUtil.equals(ov, nv)) {
					keys.add(n);
				}
			}
			if (old != null) {
				for (String n : old.stringPropertyNames()) {
					if (!prop.contains(n)) {
						String ov = tmp.get(n);
						String nv = getConfig(n);
						if (!StringUtil.equals(ov, nv)) {
							keys.add(n);
						}
					}
				}
			}
			notifyListeners(keys);
		}

	}

	public boolean removeConfigGroup(String name, boolean notify) {
		if (log.isDebugEnabled()) {
			log.debug("removeConfigGroup({},{})", name, notify);
		}
		PROP prop = getConfigGroup(name);
		if (prop == null)
			return false;
		this.datas.remove(prop);
		if (notify && prop.properties != null) {
			List<String> keys = new ArrayList<String>();
			for (String n : prop.properties.stringPropertyNames()) {
				String ov = prop.properties.getProperty(n);
				String nv = getConfig(n);
				if (!StringUtil.equals(ov, nv)) {
					keys.add(n);
				}
			}
			notifyListeners(keys);
		}
		return true;
	}

	public String getConfig(String name) {
		for (PROP prop : this.datas) {
			String v = prop.properties.getProperty(name);
			if (v != null)
				return v;
		}
		return null;
	}

}
